<html>
    <head>
        <meta charset="utf-8">
        <title>Import webrtc-internals dumps</title>
        <script src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
        <!-- highcharts is used under the terms of
            http://shop.highsoft.com/faq/non-commercial
        -->
        <script src="https://code.highcharts.com/highcharts.js"></script>
        <style>
body, body svg text {
    font-family: monospace;
}
details[open] pre {
    padding-left: 10px;
    padding-bottom: 10px;
}
table details[open] summary {
    color: blue;
}
table details.host summary:after {
    content: ' (host)';
}
table details.relay summary:after {
    content: ' (relay)';
}
table details.srflx summary:after {
    content: ' (srflx)'
}

#tables>details:nth-of-type(even) {
    background-color: #f0f0f0;
}

.graph {
    margin-left: 10px;
    margin-right: 10px;
}

.candidatepairtable {
    text-align: center;
}
.candidatepairtable td {
    background-color: #ddd;
    padding: 5px;
}
ice.style['background-color'] = '#ddd';
ice.style['text-align'] = 'center';
        </style>

        <script>
function doImport(evt) {
  evt.target.disabled = 'disabled';
  var files = evt.target.files;
  var reader = new FileReader();
  reader.onload = (function(file) {
    return function(e) {
      thelog = JSON.parse(e.target.result);
      importUpdatesAndStats(thelog);
    };
  })(files[0]);
  reader.readAsText(files[0]);
}

function createContainers(connid, url) {
    var el;
    var container = document.createElement('details');
    container.open = true;
    container.style.margin = '10px';

    var summary = document.createElement('summary');
    summary.innerText = 'Connection:' + connid + ' URL: ' + url;
    container.appendChild(summary);

    var configuration = document.createElement('div');
    container.appendChild(configuration);

    // show state transitions, like in https://webrtc.github.io/samples/src/content/peerconnection/states
    var signalingState = document.createElement('div');
    signalingState.id = 'signalingstate_' + connid;
    signalingState.textContent = 'Signaling state:';
    container.appendChild(signalingState);
    var iceConnectionState = document.createElement('div');
    iceConnectionState.id = 'iceconnectionstate_' + connid;
    iceConnectionState.textContent = 'ICE connection state:';
    container.appendChild(iceConnectionState);

    // for ice candidates
    var ice = document.createElement('table');
    ice.className = 'candidatepairtable';
    var head = document.createElement('tr');
    ice.appendChild(head);

    el = document.createElement('td');
    el.innerText = 'Local address';
    head.appendChild(el);

    el = document.createElement('td');
    el.innerText = 'Local type';
    head.appendChild(el);

    el = document.createElement('td');
    el.innerText = 'Local id';
    head.appendChild(el);

    el = document.createElement('td');
    el.innerText = 'Remote address';
    head.appendChild(el);

    el = document.createElement('td');
    el.innerText = 'Remote type';
    head.appendChild(el);

    el = document.createElement('td');
    el.innerText = 'Remote id';
    head.appendChild(el);

    el = document.createElement('td');
    el.innerText = 'Requests sent';
    head.appendChild(el);

    el = document.createElement('td');
    el.innerText = 'Responses received';
    head.appendChild(el);

    el = document.createElement('td');
    el.innerText = 'Requests received';
    head.appendChild(el);

    el = document.createElement('td');
    el.innerText = 'Responses sent';
    head.appendChild(el);

    el = document.createElement('td');
    el.innerText = 'Active Connection';
    head.appendChild(el);

    container.appendChild(ice);

    var table = document.createElement('table');
    head = document.createElement('tr');
    table.appendChild(head);

    el = document.createElement('th');
    el.innerText = 'connection ' + connid;
    head.appendChild(el);

    el = document.createElement('th');
    head.appendChild(el);

    container.appendChild(table);

    var graphs = document.createElement('div');
    container.appendChild(graphs);

    containers[connid] = {
        updateLog: table,
        iceConnectionState: iceConnectionState,
        signalingState: signalingState,
        candidates: ice,
        url: url,
        configuration: configuration,
        graphs: graphs,
    };

    return container;
}

function processGUM(data) {
    var container = document.createElement('details');
    container.open = true;
    container.style.margin = '10px';

    var summary = document.createElement('summary');
    summary.innerText = 'getUserMedia calls';
    container.appendChild(summary);

    var table = document.createElement('table');
    var head = document.createElement('tr');
    table.appendChild(head);

    container.appendChild(table);

    var columns = ['origin', 'pid', 'rid', 'audio', 'video'];
    columns.forEach(function(name) {
        var el;
        el = document.createElement('th');
        el.innerText = name;
        head.appendChild(el);
    });

    data.forEach(function(event) {
        var row = document.createElement('tr');
        columns.forEach(function(attribute) {
            var cell = document.createElement('td');
            cell.innerText = event[attribute] || '(none)';
            row.appendChild(cell);
        });
        table.appendChild(row);
    });
    document.getElementById('tables').appendChild(container);
}

function processTraceEvent(table, event) {
    var row = document.createElement('tr');
    var el = document.createElement('td');
    el.setAttribute('nowrap', '');
    el.innerText = event.time;
    row.appendChild(el);

    // recreate the HTML of webrtc-internals
    var details = document.createElement('details');
    el = document.createElement('summary');
    el.innerText = event.type;
    details.appendChild(el);

    el = document.createElement('pre');
    el.innerText = event.value;
    details.appendChild(el);

    el = document.createElement('td');
    el.appendChild(details);

    row.appendChild(el);

    // guess what, if the event type contains 'Failure' one could use css to highlight it
    if (event.type.indexOf('Failure') !== -1) {
        row.style.backgroundColor = 'red';
    }
    if (event.type === 'iceConnectionStateChange') {
        switch(event.value) {
        case 'ICEConnectionStateConnected':
        case 'ICEConnectionStateCompleted':
        case 'kICEConnectionStateConnected':
        case 'kICEConnectionStateCompleted':
            row.style.backgroundColor = 'green';
            break;
        case 'ICEConnectionStateFailed':
        case 'kICEConnectionStateFailed':
            row.style.backgroundColor = 'red';
            break;
        }
    }

    if (event.type === 'onIceCandidate' || event.type === 'addIceCandidate') {
        if (event.value && event.value.candidate) {
            var parts = event.value.split(',')[2].trim().split(' ');
            if (parts && parts.length >= 9 && parts[7] === 'typ') {
                details.classList.add(parts[8]);
            }
        }
    }
    table.appendChild(row);
}

var graphs = {};
var containers = {};
function importUpdatesAndStats(data) {
    document.getElementById('userAgent').innerText = data.UserAgent;

    var connection;
    var connid, reportname, stat;
    var t, comp;
    var stats;

    // FIXME: also display GUM calls (can they be correlated to addStream?)
    processGUM(data.getUserMedia);

    // first, display the updateLog
    for (connid in data.PeerConnections) {
        var connection = data.PeerConnections[connid];
        var container = createContainers(connid, connection.url);

        containers[connid].url.innerText = 'Origin: ' + connection.url;
        containers[connid].configuration.innerText = 'Configuration: ' + JSON.stringify(connection.rtcConfiguration, null, ' ');

        document.getElementById('tables').appendChild(container);

        connection.updateLog.forEach(function(event) {
            processTraceEvent(containers[connid].updateLog, event);
        });
        connection.updateLog.forEach(function(event) {
            // update state displays
            if (event.type === 'iceConnectionStateChange') {
                containers[connid].iceConnectionState.textContent += ' => ' + event.value;
            }
        });
        connection.updateLog.forEach(function(event) {
            // FIXME: would be cool if a click on this would jump to the table row
            if (event.type === 'signalingStateChange') {
                containers[connid].signalingState.textContent += ' => ' + event.value;
            }
        });
        var stun = {};
        for (reportname in connection.stats) {
            if (reportname.indexOf('Conn-') === 0) {
                t = reportname.split('-');
                comp = t.pop();
                t = t.join('-');
                if (!stun[t]) stun[t] = {};
                stats = JSON.parse(connection.stats[reportname].values);
                switch(comp) {
                case 'requestsSent':
                case 'consentRequestsSent':
                case 'responsesSent':
                case 'requestsReceived':
                case 'responsesReceived':
                case 'localCandidateId':
                case 'remoteCandidateId':
                case 'googLocalAddress':
                case 'googRemoteAddress':
                case 'googLocalCandidateType':
                case 'googRemoteCandidateType':
                case 'googActiveConnection':
                    //console.log(t, comp, connection.stats[reportname]);
                    stun[t][comp] = stats[stats.length - 1];
                    break;
                default:
                    //console.log(reportname, comp, stats);
                }
            }
        }

        for (t in stun) {
            var row = document.createElement('tr');
            var el;

            el = document.createElement('td');
            el.innerText = stun[t].googLocalAddress;
            row.appendChild(el);

            el = document.createElement('td');
            el.innerText = stun[t].googLocalCandidateType;
            row.appendChild(el);

            el = document.createElement('td');
            el.innerText = stun[t].localCandidateId;
            row.appendChild(el);

            el = document.createElement('td');
            el.innerText = stun[t].googRemoteAddress;
            row.appendChild(el);

            el = document.createElement('td');
            el.innerText = stun[t].googRemoteCandidateType;
            row.appendChild(el);

            el = document.createElement('td');
            el.innerText = stun[t].localCandidateId;
            row.appendChild(el);

            el = document.createElement('td');
            el.innerText = stun[t].requestsSent;
            row.appendChild(el);

            el = document.createElement('td');
            el.innerText = stun[t].responsesReceived;
            row.appendChild(el);

            el = document.createElement('td');
            el.innerText = stun[t].requestsReceived;
            row.appendChild(el);

            el = document.createElement('td');
            el.innerText = stun[t].responsesSent;
            row.appendChild(el);

            el = document.createElement('td');
            el.innerText = stun[t].googActiveConnection;
            row.appendChild(el);
            /*
            el = document.createElement('td');
            el.innerText = stun[t].consentRequestsSent;
            row.appendChild(el);
            */

            containers[connid].candidates.appendChild(row);
        }
    }

    // then, update the stats displays
    for (connid in data.PeerConnections) {
        connection = data.PeerConnections[connid];
        graphs[connid] = {};
        var reportobj = {};
        var values;
        for (reportname in connection.stats) {
            t = reportname.split('-');
            comp = t.pop();

            stat = t.join('-');
            if (!reportobj.hasOwnProperty(stat)) {
                reportobj[stat] = [];
            }
            values = JSON.parse(connection.stats[reportname].values);
            startTime = new Date(connection.stats[reportname].startTime).getTime();
            endTime = new Date(connection.stats[reportname].endTime).getTime();
            values = values.map(function(currentValue, index) {
                return [startTime + 1000 * index, currentValue];
            });
            reportobj[stat].push([comp, values]);
        }


        // sort so we get a more useful order of graphs:
        // * ssrcs
        // * bwe
        // * everything else alphabetically
        var names = Object.keys(reportobj);
        var ssrcs = names.filter(function(name) {
            return name.indexOf('ssrc_') === 0;
        }).sort(function(a, b) { // sort by send/recv and ssrc
            var aParts = a.split('_');
            var bParts = b.split('_');
            if (aParts[2] === bParts[2]) {
                return parseInt(aParts[1], 10) - parseInt(bParts[1], 10);
            } else if (aParts[2] === 'send') return -1;
            return 1;
        });
        names = names.filter(function(name) {
            return name.indexOf('ssrc_') === -1 && name !== 'bweforvideo';
        });
        names = ssrcs.concat(['bweforvideo'], names);
        names.forEach(function(reportname) {
            // ignore useless graphs
            if (reportname.indexOf('Cand-') === 0 || reportname.indexOf('Channel') === 0) return;

            var series = [];
            var reports = reportobj[reportname];
            reports.sort().forEach(function (report) {
                if (report[0] === 'mediaType') {
                    series.mediaType = report[1][0][1];
                }
                if (report[0] === 'googTrackId') {
                    series.trackId = report[1][0][1];
                }
                if (report[0] === 'ssrc') {
                    series.ssrc = report[1][0][1];
                }
                if (typeof(report[1][0][1]) !== 'number') return;
                if (report[0] === 'bytesReceived' || report[0] === 'bytesSent') return;
                if (report[0] === 'packetsReceived' || report[0] === 'packetsSent') return;
                if (report[0] === 'googCaptureStartNtpTimeMs') return;

                if (report[0] === 'bitsReceivedPerSecond' || report[0] === 'bitsSentPerSecond') { // convert to kbps
                    report[0] = 'k' + report[0];
                    report[1] = report[1].map(function(el) {
                      return [el[0], Math.floor(el[1] / 1000)];
                    });
                }

                var hiddenSeries = [
                    'qpSum',
                    'framesEncoded', 'framesDecoded',
                    'audioInputLevel', 'audioOutputLevel',
                    'googEchoCancellationEchoDelayStdDev',
                    'totalSamplesDuration',
                    'googDecodingCTN', 'googDecodingCNG', 'googDecodingNormal',
                    'googDecodingPLCCNG', 'googDecodingCTSG', 'googDecodingMuted',
                ];

                series.push({
                    name: report[0],
                    visible: hiddenSeries.indexOf(report[0]) === -1,
                    data: report[1]
                });
            });
            if (series.length > 0) {
                var container = document.createElement('details');
                container.open = reportname.indexOf('ssrc_') === 0 ||
                    reportname === 'bweforvideo' ||
                    (reportname.indexOf('Conn-') === 0 && reportname.indexOf('-1-0') !== -1);
                containers[connid].graphs.appendChild(container);
                //document.getElementById('container').appendChild(container);

                var title =
                    (series.mediaType ? 'media kind=' + series.mediaType + ' ' : '') +
                    (series.ssrc ? 'ssrc=0x' + series.ssrc.toString(16) + ' ' : '') +
                    (series.trackId ? 'trackId=' + series.trackId + ' ' : '') +
                    reportname + ' ';
                var titleElement = document.createElement('summary');
                titleElement.innerText = title;
                container.appendChild(titleElement);

                var d = document.createElement('div');
                d.id = 'chart_' + Date.now();
                d.classList.add('graph');
                container.appendChild(d);
                var graph = new Highcharts.Chart({
                    title: {
                        text: null
                    },
                    xAxis: {
                        type: 'datetime'
                    },
                    yAxis: {
                        min: series.mediaType ? 0 : undefined
                    },
                    chart: {
                        zoomType: 'x',
                        renderTo : d.id,
                    },
                    series: series
                });
                graphs[connid][reportname] = graph;

                // expand the graph when opening
                container.ontoggle = () => container.open && graph.reflow();

                // draw checkbox to turn off everything
                ((reportname, container, graph) => {
                    var checkbox = document.createElement('input');
                    checkbox.type = 'checkbox';
                    container.appendChild(checkbox);
                    var label = document.createElement('label');
                    label.innerText = 'Turn on/off all data series'
                    container.appendChild(label);
                    checkbox.onchange = function() {
                        graph.series.forEach(function(series) {
                            series.setVisible(!checkbox.checked, false);
                        });
                        graph.redraw();
                    };
                })(reportname, container, graph);
            }
        });
    }
}
        </script>
    </head>
    <body>
        <p>This is an import tool for dumps from chrome://webrtc-internals. See <a href="http://testrtc.com/webrtc-internals-parameters/">this blog post</a> for a lengthy description of what it does and how to interpret some of the data.
        <form><input type="file" onchange="doImport(event)"></form>
        <div><b>User Agent:</b> <span id="userAgent"></span></div>
        <div id="tables">
        </div>
    </body>
</html>
